package mygame;

import com.jme3.collision.CollisionResults;
import com.jme3.input.InputManager;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import gameobject.construct.Construct;

/** This control can be attached to spatials to cause them to follow the user's cursor. */
public class CursorControl extends AbstractControl {
    /** A correction factor for ray casts which hit a geometry which exactly fits a voxel space. */
    public static final float EPSILON = 0.01f;
    protected InputManager inputManager;
    protected Camera camera;
    protected Vector2f lastCursorPosition = Vector2f.ZERO;
    protected Vector3f lastCamPosition = Vector3f.ZERO;
    protected boolean topOnly = true;
    
    public CursorControl(InputManager inputManager, Camera camera) {
        this.inputManager = inputManager;
        this.camera = camera;
    }

    @Override
    protected void controlUpdate(float tpf) {
	if(camOrCursorHasChanged()) {
	    lastCursorPosition = inputManager.getCursorPosition().clone();
	    lastCamPosition = camera.getLocation().clone();

	    Vector3f targetPoint = rayCastFromMouseFirstCollisionPlusEpsilon(DataStore.worldStateNode.getHubNodeUnselectableConstruct());
	    if(targetPoint != null) {
		Voxel targetVoxel = DataStore.worldStateNode.getVoxelGrid().getVoxelAtLocation(VoxelGrid.getVoxelGridLocation(targetPoint));
		if(targetVoxel != null) { //we COULD hit a geometry which is not inside an occupied voxel. if so, ignore it
		    String voxelFace = VoxelGrid.getFaceClosestToPoint(targetVoxel, targetPoint);
		    if((topOnly && voxelFace.equals("yPos")) || !topOnly) {
			Vector3i locationAdjVoxel = VoxelGrid.getLocationAdjVoxel(targetVoxel, voxelFace);
			if(spatial instanceof Construct) { //this control can handle any spatial, but a Construct gets special treatment to update its occupiedVoxels
			    ((Construct)spatial).setBaseVoxelGridLocation(locationAdjVoxel);
			} else {
			    spatial.setLocalTranslation(VoxelGrid.getWorldLocation(locationAdjVoxel));
			}
		    }
		}
	    }
	}
    }
    
    protected Vector3f rayCastFromMouseFirstCollisionPlusEpsilon(Node onNode) {
        CollisionResults results = new CollisionResults();
	Vector2f cursor2d = inputManager.getCursorPosition();
	Vector3f cursor3d = camera.getWorldCoordinates(new Vector2f(cursor2d.x, cursor2d.y), 0f).clone();
	Vector3f dir = camera.getWorldCoordinates(new Vector2f(cursor2d.x, cursor2d.y), 1f).subtractLocal(cursor3d).normalizeLocal();
	Ray ray = new Ray(cursor3d, dir);
        onNode.collideWith(ray, results);
        if (results.size() > 0) {
	    Vector3f targetPoint = results.getClosestCollision().getContactPoint();
	    //fudge the target point because if it falls right on the line between voxels, the results are unpredictable
	    return targetPoint.addLocal(dir.normalize().multLocal(EPSILON));
	    //TODO this epsilon fudging isn't really the best method. find a better one!
        } else {
	    return null;
	}
    }

    protected boolean camOrCursorHasChanged() {
	return !lastCursorPosition.equals(inputManager.getCursorPosition()) || !lastCamPosition.equals(camera.getLocation());
    }
    
    /* 
     * Any RigidBodyControls on this or any children do not have their physicsLocations automatically updated
     * and will become out of sync. This method re-syncs all RigidBodyControls with their spatials' current location.
     */
    /* should be obsolete....................................................
    private void syncRigidBodyControls() {
        if(this.spatial.getControl(RigidBodyControl.class) != null)
            this.spatial.getControl(RigidBodyControl.class).setPhysicsLocation(this.spatial.getWorldTranslation());
	if(this.spatial instanceof Node)
            syncRigidBodyControlsOfChildren((Node)this.spatial);
    }

    private void syncRigidBodyControlsOfChildren(Node node) {
	//iterate through this node's children and manually update any RigidBodyControls
	for(Spatial child : node.getChildren()) {
	    if(child.getControl(RigidBodyControl.class) != null)
		child.getControl(RigidBodyControl.class).setPhysicsLocation(child.getWorldTranslation());
	    if(child instanceof Node)
		syncRigidBodyControlsOfChildren((Node)child);
	}
    }
    */

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        //nothing to see here
    }

    @Override
    public Control cloneForSpatial(Spatial spatial) {
        CursorControl control = new CursorControl(inputManager, camera);
        // copy custom properties here?
        spatial.addControl(control);
        return control;
    }
}
